# coding: utf-8

__author__ = 'caimingchao0702@qq.com'

import os
import time
import json
import random
import socket
import logging

from .log import Log

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')


class NodeRole:
    LEADER = 'leader'
    FOLLOWER = 'follower'
    CANDIDATE = 'candidate'


class Node:

    def __init__(self, conf: dict):
        self.logger = logging.getLogger()

        self.node_id = conf['id']
        self.addr = conf['addr']
        self.peers = conf['peers']

        self.role = NodeRole.FOLLOWER
        # persistent state on all server
        self.current_term = 0
        self.voteFor = None
        self.load()
        self.leader_id = None
        self.log = Log(self.node_id)
        # volatile state on all server
        self.commit_index = 0
        self.last_applied = 0
        # volatile state on leader
        self.next_index = {_id: self.log.last_log_index + 1 for _id in self.peers}
        self.match_index = {_id: -1 for _id in self.peers}

        self.vote_ids = {_id: 0 for _id in self.peers}

        self.recv_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.recv_socket.bind(self.addr)
        self.recv_socket.settimeout(2)

        self.send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

        self.client_addr = None

        self.wait_ms = (10, 20)
        self.next_leader_election_time = time.time() + random.randint(*self.wait_ms)
        self.next_heartbeat_time = 0

    def load(self):
        file_path = self.node_id + '/key.json'
        if os.path.exists(file_path):
            with open(file_path, 'r') as f:
                data = json.load(f)
            self.current_term = data['current_term']
            self.voteFor = data['voteFor']

    def save(self):
        if not os.path.exists(self.node_id):
            os.mkdir(self.node_id)
        data = {
            'current_term': self.current_term,
            'voteFor': self.voteFor
        }
        with open(self.node_id + '/key.json', 'w') as f:
            json.dump(data, f)

    def redirect(self, data, address):
        if not data:
            return

        if data['type'] == 'client_append_entries':
            if self.role != 'leader':
                if self.leader_id:
                    self.send(data, self.peers[self.leader_id])
                return None
            else:
                self.client_addr = address
                return data

        if data['dest_id'] != self.node_id:
            self.send(data, self.peers[data['dest_id']])
            return None
        else:
            return data

    def send(self, msg, addr):
        msg = json.dumps(msg).encode('utf8')
        self.send_socket.sendto(msg, addr)

    def recv(self):
        msg, address = self.recv_socket.recvfrom(65535)
        return json.loads(msg), address

    def request_vote(self, data):
        logging.info('recv request_vote msg[%s]' % data)
        response = {
            'type': 'request_vote_response',
            'src_id': self.node_id,
            'dest_id': data['src_id'],
            'term': self.current_term,
            'voteGranted': False
        }

        if data['term'] < self.current_term:
            logging.info('接收到的投票请求包比当前的term小，不投票 self_term[%s] dest_term[%s]' %
                         (self.current_term, data['term']))
            response['voteGranted'] = False
            self.send(response, self.peers[data['src_id']])

        # logging.info('相同的term')
        candidate_id = data['candidate_id']
        last_log_index = data['last_log_index']
        last_log_term = data['last_log_term']
        if self.voteFor is None or self.voteFor == candidate_id:
            if last_log_index >= self.log.last_log_index and last_log_term >= self.log.last_log_term:
                self.voteFor = data['src_id']
                self.save()
                response['voteGranted'] = True
                self.send(response, self.peers[data['src_id']])
            else:
                self.voteFor = None
                self.save()
                response['voteGranted'] = False
                self.send(response, self.peers[data['src_id']])
        else:
            logging.info('虽然任期相同，但是已经投过票了')
            response['voteGranted'] = False
            self.send(response, self.peers[data['src_id']])

    def append_entries(self, data):
        logging.info('recv entries msg[%s]', data)
        response = {
            'type': 'append_entries_response',
            'src_id': self.node_id,
            'dest_id': data['src_id'],
            'term': self.current_term,
            'success': False
        }
        if data['term'] < self.current_term:
            response['success'] = False
            self.send(response, self.peers[data['src_id']])

        self.leader_id = data['leader_id']
        if not data['entries']:
            logging.info('心跳')
            return

        prev_log_term = data['prev_log_term']
        prev_log_index = data['prev_log_index']

        tmp_prev_log_term = self.log.get_log_term(prev_log_index)

        if tmp_prev_log_term != prev_log_term:
            response['success'] = False
            self.send(response, self.peers[data['src_id']])
            self.log.delete_entries(prev_log_index)
        else:
            response['success'] = True
            self.send(response, self.peers[data['src_id']])
            self.log.append_entries(prev_log_index, data['entries'])

            leader_commit = data['leader_commit']
            if leader_commit > self.commit_index:
                self.commit_index = min(leader_commit, self.log.last_log_index)
        return

    def all_do(self, data):
        if self.commit_index > self.last_applied:
            self.last_applied = self.commit_index
        if data is None:
            return
        if data['type'] == 'client_append_entries':
            return
        if data['term'] > self.current_term:
            self.current_term = data['term']
            self.voteFor = None
            self.role = NodeRole.FOLLOWER
            self.save()
        return

    def candidate_do(self, data):
        t = time.time()
        for dest_id in self.peers:
            if self.vote_ids[dest_id] == 0:
                request = {
                    'type': 'request_vote',
                    'src_id': self.node_id,
                    'dest_id': dest_id,
                    'term': self.current_term,
                    'candidate_id': self.node_id,
                    'last_log_index': self.log.last_log_index,
                    'last_log_term': self.log.last_log_term
                }
                self.send(request, self.peers[dest_id])
        if data is not None and self.current_term == data['term']:
            if data['type'] == 'request_vote_response':
                logging.info('candidate: 1. recv request_vote_response from follower ' + data['src_id'])
                self.vote_ids[data['src_id']] = data['voteGranted']
                vote_count = sum(list(self.vote_ids.values()))
                if vote_count >= len(self.peers) // 2:
                    logging.info('node[%s] become leader' % self.node_id)
                    self.role = NodeRole.LEADER
                    self.voteFor = None
                    self.save()
                    self.next_heartbeat_time = 0
                    return
            elif data['type'] == 'append_entries':
                # 能收到log追加，说明系统中已经存在leader了
                logging.info('node[%s] become follower' % self.node_id)
                self.role = NodeRole.FOLLOWER
                self.next_leader_election_time = t + random.randint(*self.wait_ms)
                self.voteFor = None
                self.save()
                return
        if t > self.next_leader_election_time:
            self.next_leader_election_time = t + random.randint(*self.wait_ms)
            logging.info('node[%s] become candidate' % self.node_id)
            self.current_term += 1
            self.role = NodeRole.CANDIDATE
            self.voteFor = self.node_id
            self.save()

    def follower_do(self, data):
        t = time.time()
        if data is not None and data['type'] == 'append_entries':
            if data['term'] == self.current_term:
                self.next_leader_election_time = t + random.randint(*self.wait_ms)
            self.append_entries(data)
        elif data is not None and data['type'] == 'request_vote':
            self.request_vote(data)

        if t > self.next_leader_election_time:
            self.next_leader_election_time = t + random.randint(*self.wait_ms)
            logging.info('node[%s] become candidate' % self.node_id)
            self.role = NodeRole.CANDIDATE
            self.current_term += 1
            self.voteFor = self.node_id
            self.save()
            self.vote_ids = {_id: 0 for _id in self.peers}

    def leader_do(self, data):
        t = time.time()
        if t > self.next_heartbeat_time:
            for dest_id in self.peers:
                request = {
                    'type': 'append_entries',
                    'src_id': self.node_id,
                    'dest_id': dest_id,
                    'term': self.current_term,
                    'leader_id': self.node_id,
                    'prev_log_index': self.next_index[dest_id] - 1,
                    'prev_log_term': self.log.get_log_term(self.next_index[dest_id] - 1),
                    'entries': self.log.get_entries(self.next_index[dest_id]),
                    'leader_commit': self.commit_index
                }
                self.send(request, self.peers[dest_id])

        if data is not None and data['type'] == 'client_append_entries':
            data['term'] = self.current_term
            self.log.append_entries(self.log.last_log_index, [data])
            return

        if data is not None and data['term'] == self.current_term:
            if data['type'] == 'append_entries_response':
                if data['success']:
                    self.match_index[data['src_id']] = self.next_index[data['src_id']]
                    self.next_index[data['src_id']] = self.log.last_log_index + 1
                else:
                    self.next_index[data['src_id']] -= 1

        while True:
            N = self.commit_index + 1
            count = 0
            for _id in self.match_index:
                if self.match_index[_id] >= N:
                    count += 1
                if count >= len(self.peers)//2:
                    self.commit_index = N
                    if self.client_addr:
                        response = {'index': self.commit_index}
                        self.send(response, (self.client_addr[0], 10000))
                    break
            else:
                break

    def run(self):
        while True:
            try:
                try:
                    data, address = self.recv()
                except Exception as e:
                    # logging.error(e)
                    data, address = None, None
                self.redirect(data, address)
                self.all_do(data)

                if self.role == NodeRole.FOLLOWER:
                    self.follower_do(data)
                elif self.role == NodeRole.CANDIDATE:
                    self.candidate_do(data)
                elif self.role == NodeRole.LEADER:
                    self.leader_do(data)

            except Exception as e:
                logging.error(e)
                raise e
